/*
 * Crystal Monster 
 *   -- LED Light control program for Beverly Tang's "Crystal Monster" sculpture
 * 
 * Circuit Description:
 * - The controller (Arduino with this sketch) controls all of the sculpture
 * - The controller is powered by the +5VDC "standby voltage" (purple wire) 
 *    that is always enabled on an ATX supply
 * - 18 BlinkM MaxMs are wired to the Arduino's I2C bus with external pull-up
 *    resistors (currently 2.2k, perhaps 1.0k should be used)
 * - MaxMs are given consecutive I2C addresses, starting at a "base" address
 * - The MaxMs are powered by +12VDC rail (yellow wire) of an ATX power supply
 *    Custom Molex-to-DC-barrel connector cables created to distribute power
 * - The controller toggles the "Power Supply On" (green wire) of the ATX 
 *    supply to power on/off the power to the MaxMs
 * - A PNA4602 38kHz IR detector is wired to a digital input of the controller,
 *    and the controller accepts commands via the Sony TV IR protocol
 * - The controller outputs via Serial its state, and accepts a few commands
 * - A 4-pin connector containing Gnd, Serial RX,Serial TX, and Reset 
 *    (thru 0.1uF cap) connects to a cable to the floor, allowing
 *    remote sketch updates using standard Arduino IDE
 * - Alternatively, the 4-pin cable can be used to create a monitor
 *    that has the ability to reset the controller if needed
 *
 *
 * Crystal Monster sections & MaxM layout:
 *   ordinal number:     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
 *   monster sections:   A  B  C  D  E  F  G  H  J  K  L  M  N  P  Q  R  S  T 
 *   maxm hex i2c addr: 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21
 *
 *
 * Deployed on a yellow Freeduino Arduino (from kit) with ATmega328
 * - To compile, choose "Arduino Duemillinove w/328" from Boards menu
 * - To program, hook FTDI cable up to 4-pin cable and click "Upload"
 *
 * 2009, Tod E. Kurt,  http://todbot.com/blog/
 *
 * -------------------------------------------------------------------------
 * Notes - 7 Jun 2009:
 * - Flickering of LEDs at higher brightness levels(?), perhaps droopy +12V
 *    rail from ATX?  Could possibly bridge both +12VDC rails, but seems
 *    to happen at lower avg current.  Maybe big inrush currents due to PWM?
 *
 * - Aperiodic hanging of I2C bus (and thus entire board) still a mystery.
 *    Slowing down I2C bus transactions (via TWI_FREQ) doesn't appear to 
 *    obviously help.  Speeding up seemed to help a little (hints at bad power)
 *    Current hacked value of TWI_FREQ is 400000L in :
 *       .../arduino-0016/hardware/libraries/Wire/utility/twi.h
 *    (be sure to reselect board in Boards menu to cause this to recompile)
 * 
 * - Increasing time between transactions by replacing
 *    for()-loops for broadcasts and adding "i2cmillis" delay seems to help
 *    a little.  But the added delays screws up IR remote handling response.
 *
 * - I2C bus capacitance shouldn't be an issue.  Ribbon cable should be good
 *    to 8m theoretically.  Current cable is ~2m.
 *   
 * - 22uF caps added to 5V supply of every MaxM via 5V/Gnd socket. 
 *    Doesn't seem to help much wrt I2C hanging. 
 * 
 * - Can't use Watchdog Timer (WDT) on Arduino because bootloader doesn't 
 *    disable WDT and doesn't leave bootloader before WDT triggers.
 *    (The 328 chip installed after WDT mishap might do the disable, but I
 *    don't wan to try it unless we have scissor lift again).
 *
 * - Reset via the cable works, but not by just shorting to Gnd.  
 *    The answer is obvious I'm sure, but I've not thought about it.
 *
 * - The "off" command via serial works, but seems to hang the chip,
 *    making subsequent "on" serial command not work.
 *
 */


#include <avr/pgmspace.h>
#include <EEPROM.h>
#include <Wire.h>
#include "BlinkM_funcs.h"
#include "IRRemoteSony.h"


// how many blinkms we have and their starting address, assume consecutive
const int blinkmNum = 17;         // number of BlinkMs connected
const int blinkmBaseAddr = 0x11;  // starting I2C address (assumes contiguous)

const int i2cmillis = 80;         // yes, we're really going to wait this long

// pins used on Arduino
const int sensorRXPin  = 5;
const int sensorTXPin  = 6;
const int irDetectPin  = 8;
const int powerPin     = 12;   // 1k pull-down on pin (but doesn't work)
const int ledPin       = 13;

// state of things
boolean poweron = false;
boolean playing = false;

// useful variables that may be used by the scripts
// some are stored as EEPROM startup settings
byte maxBright = 255;      // maximum allowed brightness
byte scriptNum;            // current script we're on
int stepTime = 1000;       // time between steps
byte fadeSpeed = 3;     // FIXME: should have algo determine this from stepTime?

int scriptPos;             // position within the script
unsigned long lastMillis;  // time of last position playted

// for IR key debouncing
unsigned long keyTime;

// function pointers for scripts (danger, really nerdy)
typedef int  (*script_ptr)(int, int); // = NULL;
script_ptr script;

// a list of all the scripts 
const script_ptr  scripts[] PROGMEM = 
  {
    script_0,
    script_1,
    script_2,
    script_3,
    script_4,
    script_5,
    script_6,
  };

// number of scripts
byte scriptCnt = sizeof(scripts) / sizeof(script_ptr); 



// we do this brightness mapping a lot, so make it a macro
#define brimap(a)  map(a, 0,0xff, 0,maxBright)

//
void setup()
{
  Serial.begin(19200);
  Serial.print("Crystal Monster v2 starting up. script count:");
  Serial.println( scriptCnt, DEC );
  
  pinMode(powerPin, OUTPUT);
  pinMode(irDetectPin, INPUT);
  pinMode(ledPin, OUTPUT);
  
  load_settings();
  // if empty EEPROM (i.e. invalid values), load up defaults
  if( maxBright == 0 || scriptNum >= scriptCnt || stepTime < 100 ) { 
    scriptNum = 0;
    stepTime  = 1000;
    maxBright = 100;
    fadeSpeed = 3;
  }
  
  power_on();   // power up main system
  delay(500);   // wait for power to stabilize
  
  BlinkM_begin();
  
  blinkm_setall_black();
  
  digitalWrite(ledPin, HIGH);  // say we're alive
  Serial.println("Crystal Monster ready!");
  
  script_change(scriptNum);    // start playing script
  playing = true;
  
}

//
void loop()
{
  // handle commands from the monitor box
  if( Serial.available() > 2 ) { 
    int c1 = Serial.read();
    int c2 = Serial.read();
    if( c1 == 'b' && c2 == 'o' ) {        // do a "boom"
      boom(10);
    } 
    else if( c1 == 'o' && c2 == 'f' ) {   // turn "off"
      playing = false;
      power_off();
      delay(3000);
    } 
    else if( c1 == 'o' && c2 == 'n' ) {   // turn "on"
      power_on();
      delay(3000);
      blinkm_setall_black();
      playing = true;
    }
    else if( c1 == 'u' && c2 == 'p' ) {  // script up
      scriptNum++;
      if( scriptNum == scriptCnt ) scriptNum = 0;
      script_change(scriptNum);
    }
  } // if( Serial.available() )

  int key = getIRKey( irDetectPin, 100 );  // Look for key, for at most 100ms
  if( key != -1 ) {
    digitalWrite( ledPin, HIGH);   // say we did something
    unsigned long t = millis();
    if( (t - keyTime) > 300 ) {  // do anti-keybounce sorta thing
      keyTime = t;
      Serial.print("irkey: ");
      Serial.println(key);

      // handle keys
      if( key == IR_POWER ) {
        if( poweron ) {
          playing = false;
          power_off();
          delay(3000);
        }
        else {
          power_on();
          delay(3000);
          blinkm_setall_black();
          playing = true;
        }
      }
      else if( key == IR_PLAY ) {
        script(0,0);
        playing = true;
      }
      else if( key == IR_STOP ) {
        playing = false;
      }
      else if( key == IR_RECORD ) { 
        save_settings();
        boom(20);
      }
      else if( key == IR_ENTER ) {
        boom(10);
      }
      else if( key == IR_PAUSE ) {
        playing = !playing;
      }
      else if( key >= IR_ONE && key <= IR_NINE ) {   // handle keypad 1-9 nums
        scriptNum = key - IR_ONE + 1;
        script_change(scriptNum);
      }
      else if( key == IR_ZERO ) {             // handle keypad 0
        scriptNum = 0;
        script_change(scriptNum);
      }
      else if( key == IR_VOLUP ) {            // vol up/dn adjust max brightness
        maxBright = (maxBright >= 254 ) ? 255 : maxBright + 2;
        Serial.print("maxBright:"); Serial.println(maxBright,DEC);
      }
      else if( key == IR_VOLDN ) {
        maxBright = (maxBright < 2 ) ? 0 : maxBright - 2;
        Serial.print("maxBright:"); Serial.println(maxBright,DEC);
      } 
      else if( key == IR_CHUP ) {             // ch-up/dn adjust stepTime
        stepTime = (stepTime < 100 ) ? 100 : stepTime - 100;
        Serial.print("stepTime:"); Serial.println(stepTime,DEC);
      }
      else if( key == IR_CHDN ) { 
        stepTime = (stepTime > 20000 ) ? 20000 : stepTime + 100;;
        Serial.print("stepTime:"); Serial.println(stepTime,DEC);
      }
      else if( key == IR_FF ) {              // ff/rw adjust 
        fadeSpeed = (fadeSpeed > 200 ) ? 200 : fadeSpeed + 1;
        blinkm_setall_fadespeed( fadeSpeed );
      }
      else if( key == IR_RW ) {
        Serial.println("rw");
        fadeSpeed = (fadeSpeed <= 1 ) ? 1 : fadeSpeed - 1;
        blinkm_setall_fadespeed( fadeSpeed );
      }
      //Serial.print("stepTime:"); Serial.println(stepTime);
    }
  } // if( key )

  if( playing ) { 
    scriptPos = script( scriptPos, millis() ); 
  }
  delay(10); // FIXME:

}

/////////////////////////////////////////////////////////////////////////////
// misc utility functions

void boom(int m) { 
  byte n = random(0x11);
  BlinkM_setRGB( 0, 10+m+n,10+m+n,10+m+n );
  delay(i2cmillis*2);
  BlinkM_fadeToRGB(0, 0,0,0);
}

void power_on() {
  Serial.println("powering on");
  digitalWrite( powerPin, LOW );
  poweron = true;
}
void power_off() {
  Serial.println("powering off");
  poweron = false;
  digitalWrite( powerPin, HIGH );
}

void save_settings() {
  Serial.println("saving settings");
  EEPROM.write( 0, scriptNum );
  EEPROM.write( 1, stepTime/10 );
  EEPROM.write( 2, maxBright );
  EEPROM.write( 3, fadeSpeed );
}

void load_settings() {
  Serial.println("loading settings");
  scriptNum = EEPROM.read(0);
  stepTime  = EEPROM.read(1) * 10;
  maxBright = EEPROM.read(2);
  fadeSpeed = EEPROM.read(3);
}

///////////////////////////////////////////////////////////////////////////
// blinkm & script functions

void blinkm_setall_fadespeed( int f ) {
  Serial.print("setting all fadespeed:"); Serial.println(f);
  BlinkM_setFadeSpeed( 0, f ); // broadcast
}

// set all BlinkMs to known state
void blinkm_setall_black() {
  BlinkM_stopScript( 0 );
  delay(i2cmillis);
  BlinkM_fadeToRGB( 0, 0,0,0); // fade to black
}

// this is the really lame, but quick, way to do this
// and we have code space to burn still
void script_change( int num ) { 
  Serial.print("script_change:");  Serial.println(num);
  if( num < 0 || num >= scriptCnt ) {
    Serial.println("bad script num");
    return;
  }
  script = (script_ptr)pgm_read_word( &scripts[num] );
  script(0,0);  // do script setup
}


/////////////////////////////////////////////////////////////////////////////
// Scripts
//
// To create a new script:
// - make a function of the form "int scriptname(int pos, int mills)"
// - in that function have two if-blocks: 
// -- one for setup. put any script-specific setup in there.
//    it typically looks like:
//      "if( mills==0 && pos==0) { ... }"
// -- one for each time through the main loop.  generally you don't want
//    to run at full loop speed, so check the current time ("mills") vs 
//    the last time ("lastMillis"), and compare it to "stepTime", like:
//      "if( mills - lastMillis > stepTime ) {  lastMillis = milis; .... }"
// -- inside the loop part, be sure to add a "Serial.print()" that succinctly
//      describes your script doing something, so the monitor box knows about it
//      (you can also use the Arduino Serial Monitor to watch)
// -- when done with new script, add it "scripts" list up top.
//
//


//
// fixed color at all positions
//
int script_6(int pos, int mills)
{
  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis >  (stepTime/10) ) { // ting!
    lastMillis = mills;
    
    byte r = 255;         // red, goes from 0-255
    byte g = 0;           // grn, goes from 0-255 
    byte b = 0;           // blu, goes from 0-255
    
    byte n = 0;  // 0 means all sections
    Serial.println("s6:fixed");
    
    BlinkM_fadeToRGB( blinkmBaseAddr + n,  r,g,b );
    delay(i2cmillis);
  }

  return pos;
}

//
// random color explosion,with falloff
//
int script_5(int pos, int mills)
{
  // case for script setup
  if( mills==0 && pos==0 ) {        
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis >  (stepTime/10) ) { // ting!
    lastMillis = mills;
    
    byte r = random(0xff);
    byte g = random(0xff);
    byte b = random(0xff);
    
    pos = 1 + random(blinkmNum-2);
    
    Serial.println("s5:explode");

    // center of explosion
    BlinkM_setRGB( blinkmBaseAddr+pos, brimap(r),brimap(g),brimap(b) );
    delay(i2cmillis);
    // area around it
    BlinkM_setRGB( blinkmBaseAddr+pos-1, brimap(r/2),brimap(g/2),brimap(b/2) );
    delay(i2cmillis);
    BlinkM_setRGB( blinkmBaseAddr+pos+1, brimap(r/2),brimap(g/2),brimap(b/2) );
    delay(i2cmillis);

    // fade explosion away
    for( int i=0; i<blinkmNum; i++ ) {
      BlinkM_fadeToRGB( blinkmBaseAddr+i, 0x00,0x00,0x00 );
      delay(i2cmillis);
    }
  }

  return pos;
}

//
// random color at random position
//
int script_4(int pos, int mills)
{
  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis >  (stepTime/10) ) { // ting!
    lastMillis = mills;
    
    byte r = random(maxBright);
    byte g = random(maxBright);
    byte b = random(maxBright);
    
    byte n = random(blinkmNum);
    Serial.println("s4:random");
    
    BlinkM_fadeToRGB( blinkmBaseAddr + n,  r,g,b );
    delay(i2cmillis);
  }

  return pos;
}

//
// thwump explosion like thing
//
int script_3(int pos, int mills)
{
  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis >  (stepTime/10) ) { // ting!
    lastMillis = mills;
 
    byte r = maxBright;
    byte g = maxBright;
    byte b = maxBright;
    
    byte i = random(blinkmNum);
    Serial.println("s3:thwump");

    BlinkM_setRGB( blinkmBaseAddr+i, r,g,b );
    delay(i2cmillis);
    BlinkM_fadeToRGB( blinkmBaseAddr+i, 0x00,0x00,0x00 );
  }
  return pos;
}


//
// slow
//
int script_2(int pos, int mills)
{
  static int incdec = 1;                  // save whether we're going up or down

  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis > stepTime ) { // ting!
    lastMillis = mills;
    pos+=incdec;
    if( pos == blinkmNum-1 ) incdec = -1;
    else if( pos == 0 ) incdec = 1;
    Serial.print("s2:slow:");
    Serial.println(pos);
    BlinkM_fadeToRGB( 0, 
                      brimap(0x11),brimap(0x22),brimap(0x33) );
    delay(i2cmillis);
    BlinkM_fadeToRGB( blinkmBaseAddr+pos, 
                      brimap(0x44),brimap(0x22),brimap(0x22) );
  }
  return pos;
}

//
// trails
//
int script_1(int pos, int mills)
{
  static int incdec = 1;
  static byte lastpos, lastlastpos;

  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );  // set known fade speed
    delay(i2cmillis);
    BlinkM_fadeToRGB( 0, 0,0,0);          // fade to black
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis > (stepTime/10) ) { // ting! time to do something
    lastlastpos = lastpos;
    lastpos = pos;
    lastMillis = mills;
    pos+=incdec;
    if( pos == blinkmNum-1 ) incdec = -1;
    else if( pos == 0 ) incdec = 1;
    Serial.print("s1:trail:");
    Serial.println(pos);
    BlinkM_fadeToRGB( blinkmBaseAddr+lastlastpos, 0,0,0 );
    delay(i2cmillis);
    BlinkM_fadeToRGB( blinkmBaseAddr+lastpos, 0x0b,0x0b,0x0b );
    delay(i2cmillis);
    BlinkM_fadeToRGB( blinkmBaseAddr+pos, 
                      brimap(0x11), brimap(0x22), brimap(0x33) );
  }
  return pos;
}

//
// sequential color test
//
int script_0(int pos, int mills)
{
  static int incdec = 1;                  // save whether we're going up or down
  static int colornum = 0;                // save the color we were last on

  // case for script setup
  if( mills==0 && pos==0 ) {
    BlinkM_setFadeSpeed( 0, fadeSpeed );   // set fadespeed to known state
  }

  // otherwise once-per-loop processing happens, after a stepTime period
  if( mills - lastMillis >  (stepTime/10) ) { // ting!
    lastMillis = mills;
    colornum = (colornum + 1) % 4;  // 3 values: 0,1,2,3
    if( colornum == 0 ) {
      pos+=incdec;
      if( pos == blinkmNum-1 ) incdec = -1;
      else if( pos == 0 ) incdec = 1;
      Serial.print("s0:test:");
      Serial.println(pos);
    }

    BlinkM_fadeToRGB( 0, 0,0,0 );
    delay(i2cmillis);

    byte nr = brimap(0xff); 
    byte ng = brimap(0xff); 
    byte nb = brimap(0xff); 
    if( colornum == 0 ) 
      BlinkM_fadeToRGB( blinkmBaseAddr+pos, nr,ng,nb );
    else if( colornum == 1 )
      BlinkM_fadeToRGB( blinkmBaseAddr+pos, nr,0x00,0x00 );
    else if( colornum == 2 )
      BlinkM_fadeToRGB( blinkmBaseAddr+pos, 0x00,ng,0x00 );
    else if( colornum == 3 )
      BlinkM_fadeToRGB( blinkmBaseAddr+pos, 0x00,0x00,nb );
  }
  return pos;
}



